package com.alogic.jsoup;

import com.alogic.xscript.Logiclet;
import com.alogic.xscript.LogicletContext;
import com.alogic.xscript.doc.XsObject;
import com.alogic.xscript.doc.json.JsonObject;
import com.anysoft.util.DefaultProperties;
import com.anysoft.util.Pair;
import com.anysoft.util.Properties;
import com.anysoft.util.PropertiesConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jsoup.Jsoup;
import org.jsoup.helper.Validate;
import org.jsoup.nodes.*;
import org.jsoup.parser.Tag;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;

/**
 * Html清理器
 *
 * @since 1.6.13.13 [20200904 duanyy] <br>
 */
public class HtmlCleaner {
    /**
     * a logger of slf4j
     */
    protected final static Logger LOG = LoggerFactory.getLogger(HtmlCleaner.class);

    public static String clean(String bodyHtml, String baseUri, WhiteList whitelist, Properties ctx) {
        Document dirty = Jsoup.parseBodyFragment(bodyHtml, baseUri);
        HtmlCleaner cleaner = new HtmlCleaner();
        Document clean = cleaner.clean(dirty,whitelist,ctx);
        return clean.body().html();
    }
    public static String clean(String bodyHtml, WhiteList whitelist, Properties ctx) {
        return clean(bodyHtml, "", whitelist,ctx);
    }

    public static String clean(String bodyHtml, String baseUri, WhiteList whitelist, Document.OutputSettings outputSettings, Properties ctx) {
        Document dirty = Jsoup.parseBodyFragment(bodyHtml, baseUri);
        HtmlCleaner cleaner = new HtmlCleaner();
        Document clean = cleaner.clean(dirty,whitelist,ctx);
        clean.outputSettings(outputSettings);
        return clean.body().html();
    }

    public Document clean(Document dirtyDocument,WhiteList whitelist, Properties ctx) {
        Validate.notNull(dirtyDocument);
        Document clean = Document.createShell(dirtyDocument.baseUri());
        if (dirtyDocument.body() != null) {
            copySafeNodes(dirtyDocument.body(), clean.body(),whitelist,ctx);
        }
        return clean;
    }

    protected void copySafeNodes(Element source, Element dest,WhiteList whitelist, Properties ctx) {
        CleaningVisitor cleaningVisitor = new CleaningVisitor(source, dest,whitelist,ctx);
        NodeTraversor.traverse(cleaningVisitor, source);
    }

    private final class CleaningVisitor implements NodeVisitor {
        private final Element root;
        private Element destination;
        private final WhiteList whitelist;
        private final Properties ctx;
        protected String objectId = "$md-reattr";

        private CleaningVisitor(Element root, Element destination,WhiteList whitelist, Properties ctx) {
            this.root = root;
            this.destination = destination;
            this.whitelist = whitelist;
            this.ctx = ctx;
        }

        public void head(Node source, int depth) {
            if (source instanceof Element) {
                Element sourceEl = (Element) source;
                WhiteList.Tag tag = whitelist.getTag(sourceEl.normalName());
                if (tag != null){
                    Element destChild = createSafeElement(sourceEl,tag);
                    destination.appendChild(destChild);
                    destination = destChild;
                }
            } else if (source instanceof TextNode) {
                TextNode sourceText = (TextNode) source;
                TextNode destText = new TextNode(sourceText.getWholeText());
                destination.appendChild(destText);
            } else if (source instanceof DataNode){
                WhiteList.Tag tag = whitelist.getTag(source.parent().nodeName());
                if (tag != null) {
                    DataNode sourceData = (DataNode) source;
                    DataNode destData = new DataNode(sourceData.getWholeData());
                    destination.appendChild(destData);
                }
            }
        }

        public void tail(Node source, int depth) {
            if (source instanceof Element){
                WhiteList.Tag tag = whitelist.getTag(source.nodeName());
                if (tag != null){
                    destination = destination.parent();
                }
            }
        }

        private Element createSafeElement(Element sourceEl,WhiteList.Tag tag){
            String tagName = sourceEl.tagName();
            Logiclet event = tag.getEvent();
            if (event != null){
                Attributes destAttrs = new Attributes();
                Attributes sourceAttrs = sourceEl.attributes();
                List<Pair<String,String>> extAttrs = tag.getExtAttrs();
                if (!extAttrs.isEmpty()) {
                    LogicletContext logicletContext = new LogicletContext(ctx);
                    try {
                        //将当前属性列表复制到上下文
                        for (Attribute sourceAttr : sourceAttrs) {
                            boolean isSafeAttribute = tag.existAttr(sourceAttr.getKey());
                            if (!isSafeAttribute){
                                WhiteList.Tag allTag = whitelist.getTag(":all");
                                if (allTag != null){
                                    isSafeAttribute = allTag.existAttr(sourceAttr.getKey());
                                }
                            }
                            if (isSafeAttribute) {
                                destAttrs.put(sourceAttr);
                                logicletContext.SetValue("$attr." + sourceAttr.getKey(), sourceAttr.getValue());
                            }
                        }
                        //通过事件来计算上下文变量
                        logicletContext.SetValue("$tag", tagName);
                        XsObject doc = new JsonObject("root", new HashMap<String, Object>());
                        event.execute(doc, doc, logicletContext, null);
                        //从上下文变量中取回计算结果
                        for (Pair<String, String> p : extAttrs) {
                            String value = PropertiesConstants.transform(logicletContext, p.value(), "");
                            if (StringUtils.isNotEmpty(value)) {
                                destAttrs.put(p.key(), value);
                            }
                        }
                    } catch (Exception ex) {
                        LOG.info("Failed to execute event script" + ExceptionUtils.getStackTrace(ex));
                    } finally {
                        logicletContext.removeObject(objectId);
                    }
                }else{
                    for (Attribute sourceAttr : sourceAttrs) {
                        boolean isSafeAttribute = tag.existAttr(sourceAttr.getKey());
                        if (!isSafeAttribute){
                            WhiteList.Tag allTag = whitelist.getTag(":all");
                            if (allTag != null){
                                isSafeAttribute = allTag.existAttr(sourceAttr.getKey());
                            }
                        }
                        if (isSafeAttribute) {
                            destAttrs.put(sourceAttr);
                        }
                    }
                }
                return new Element(Tag.valueOf(tagName), sourceEl.baseUri(), destAttrs);
            }else{
                Attributes destAttrs = new Attributes();
                Attributes sourceAttrs = sourceEl.attributes();
                List<Pair<String,String>> extAttrs = tag.getExtAttrs();
                if (!extAttrs.isEmpty()) {
                    Properties props = new DefaultProperties("default", ctx);
                    for (Attribute sourceAttr : sourceAttrs) {
                        boolean isSafeAttribute = tag.existAttr(sourceAttr.getKey());
                        if (!isSafeAttribute){
                            WhiteList.Tag allTag = whitelist.getTag(":all");
                            if (allTag != null){
                                isSafeAttribute = allTag.existAttr(sourceAttr.getKey());
                            }
                        }
                        if (isSafeAttribute) {
                            destAttrs.put(sourceAttr);
                            props.SetValue("$attr." + sourceAttr.getKey(), sourceAttr.getValue());
                        }
                    }
                    for (Pair<String, String> p : extAttrs) {
                        String value = PropertiesConstants.transform(props, p.value(), "");
                        if (StringUtils.isNotEmpty(value)) {
                            destAttrs.put(p.key(), value);
                        }
                    }
                }else{
                    for (Attribute sourceAttr : sourceAttrs) {
                        boolean isSafeAttribute = tag.existAttr(sourceAttr.getKey());
                        if (!isSafeAttribute){
                            WhiteList.Tag allTag = whitelist.getTag(":all");
                            if (allTag != null){
                                isSafeAttribute = allTag.existAttr(sourceAttr.getKey());
                            }
                        }
                        if (isSafeAttribute) {
                            destAttrs.put(sourceAttr);
                        }
                    }
                }

                return new Element(Tag.valueOf(tagName), sourceEl.baseUri(), destAttrs);
            }
        }
    }
}
